import hashlib
from urllib.parse import urlparse
from flask import Flask, jsonify, request, render_template
from uuid import uuid4
import requests
import copy
import dao.blockStore
import dto.block
import util.ctime
import util.chash
import modules.net.pynet
import modules.blockchain 
import modules.transaction 
import util.chash
import modules.log
from util.singelMode import singleton 
@singleton
class CBlock:
    def __init__(self):
        pass
    def __call__(self):
        return self
  
    def generateBlock(self, proof, prevBlockHash,height):
            """
            生成新块
            :param proof: <int> The proof given by the Proof of Work algorithm
            :param prevBlockHash: (Optional) <str> Hash of previous Block
            :return: <dict> New Block
            """
            timestamp=util.ctime.Ctime().getNowTimestamp()
            timestamp = str(timestamp)
            transactions= modules.transaction.CTransaction.current_transactions
            txcounts=len(transactions)
            blk = dto.block.CBlock(1,height,timestamp,txcounts,transactions,prevBlockHash,proof,"")
            blk.selfhash=util.chash.CHash.getBlockHash(blk)
            # Reset the current list of transactions
            modules.transaction.CTransaction.current_transactions= []
            return blk
    
    def reciveBlock(self,block,fromNodeAddress="",ifBroadcast=True):
        """
        @block 为python object (dict)
        """
        
        if self.validBlock(block):
            block = dto.block.CBlock.deserializeFromPyObject(block)
            isStored, storeInfo = self.ifBlockIsStoredLocal(block.selfhash)
            if isStored:
                #该区块已经储存
                modules.log.Clog.logger.info("接受一个区块 但是该区块已经存在")
                return -2,True
            else:
                isStored, blkStoreInfo = self.ifBlockIsStoredLocal(block.prevBlockHash)
                if isStored:
                    if  int(blkStoreInfo.split(',')[0])  + 1 == block.height :
                        #if the Block is considered to be the tip of a blockchain
                        if  block.prevBlockHash == modules.blockchain.CBlockchain.last_block['selfhash']:
                                return self.processBlock(block,2,fromNodeAddress,ifBroadcast)
                        else:
                            #该block是分叉上的区块
                            # storefilename = blkStoreInfo.split(',')[2]
                            prevBlockHash = blkStoreInfo.split(',')[1]
                            storefilename=f'{prevBlockHash[0:2]}/{prevBlockHash}.dat'
                            prevBlock = self.getBlockFromDiskByFileName(storefilename)
                            
                            #该block是产生分叉的区块
                            ifIsIn,flag = self.ifIsInMainChain(prevBlock.selfhash,prevBlock.height)
                            if ifIsIn :
                                topHeight = modules.blockchain.CBlockchain.getChainHeight()
                                height = prevBlock.height
                                if topHeight > height and topHeight -height < 6:
                                    return self.processBlock(block,3,fromNodeAddress,ifBroadcast)
                                else:
                                    return -2,False
                            #如果该block是分支上的block
                            else:
                                #加入block后分支的tipblock的高度小于当前主链的高度
                                if block.height  < modules.blockchain.CBlockchain.getChainHeight(): 
                                    # modules.log.Clog.logger.info("接受一个区块 但是该区块高度低于当前主链高度")
                                    # return -2,False
                                    return self.processBlock(block,4,fromNodeAddress,ifBroadcast)
                                    
                                #加入block后分支的tipblock的高度等于当前主链的高度，分支与当前主链高度并驾齐驱
                                elif  block.height  == modules.blockchain.CBlockchain.getChainHeight():
                                    return self.processBlock(block,4,fromNodeAddress,ifBroadcast)
                                #加入block后，分叉上的区块高度赶超当前主链的高度
                                elif block.height ==  modules.blockchain.CBlockchain.getChainHeight() + 1 :
                                    return self.processBlock(block,5,fromNodeAddress,ifBroadcast)    
                                else:
                                    return -2,False

                    else:
                        modules.log.Clog.logger.info("接受一个区块 但是该区块高度错误")
                        return -2,False
                else:
                    #该区块是孤块
                    modules.log.Clog.logger.info("接受一个区块 但是该区块是孤块")
                   
                    return -1,True   
                      
        else:
            modules.log.Clog.logger.info("接受一个区块 但是该区块valid 错误")
            return 0,False

    def tryRequestBlockFromNet(self,blkhash,fromNodeAddress):
        option = {
            'method': 'GET',
            'api': '/block',
            'data':{
                'blockhash':blkhash
            }
        }
        response=modules.net.pynet.CP2pnet.getFromPeer(fromNodeAddress,option)
        if response!='' and response.status_code==200:
            block = response.json()['block']
            return self.reciveBlock(block,fromNodeAddress,False)
        return -3,False

       
    def processBlock(self,block,blocktype,fromNodeAddress="",ifBroadcast=True):
        """
        @block dto.block.CBlock 实例
        """
        #===================================block from local==========================================================
        #GenesisBlock
        if blocktype == 0:
            block.selfhash=""
            blockhash = util.chash.CHash.getBlockHash(block)
            block.selfhash = blockhash
            storefilename = dao.blockStore.CBlockStore.writeBlockToDisk(block)
            if isinstance(storefilename,str):
                # if stored success
                modules.blockchain.CBlockchain.last_block=block.serializeToPyObject()

                #update blkinfo in chainIndexdata
                tipBlockInf= dto.tipblkInfo.CTipblkInfo(0,block.selfhash,storefilename)
                modules.db.CDb.setValue("chainIndexFile","tipBlock",tipBlockInf.serializeToJsonStr())

                return 0,True             

            else:
                return -2,False
        
        #mineblock
        elif blocktype == 1:
            block.selfhash=""
            blockhash = util.chash.CHash.getBlockHash(block)
            block.selfhash = blockhash
            storefilename = dao.blockStore.CBlockStore.writeBlockToDisk(block)
            if isinstance(storefilename,str):
                #if stored success
                modules.blockchain.CBlockchain.last_block=block.serializeToPyObject()
                     
                            
                # update utxsetInfo in chainstateData
                t = len(modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk)
                if  t == 5 :
                    blkhash = modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk.pop(0)
                    modules.blockchain.CBlockchain.updateUTXOset(blkhash)
                # modules.blockchain.CBlockchain.updateUTXOset(blockhash)
                #update blkinfo in chainIndexdata
                tipBlockInf= dto.tipblkInfo.CTipblkInfo(block.height,block.selfhash,storefilename)
                modules.db.CDb.setValue("chainIndexFile","tipBlock",tipBlockInf.serializeToJsonStr())
                modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk.append(block.selfhash)

                if len(modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk)==5:
                    modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk.pop(0)   
                modules.transaction.CTransaction.updatetotalstransactionnumber(block.txcounts)
                block=block.serializeToPyObject()  
                modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk.append(block)


                #广播区块
                if ifBroadcast:
                    modules.net.pynet.CP2pnet.broadcast("blk",block)
                return 1,True
            else: 
                return -2,False
        
        #===================================block from net==========================================================
        #if the Block is considered to be the tip of a blockchain
        elif blocktype == 2:
            # if block.valid():
                storefilename = dao.blockStore.CBlockStore.writeBlockToDisk(block)
                if isinstance(storefilename,str):
                    #if stored success
                    modules.blockchain.CBlockchain.last_block=block.serializeToPyObject()                    
                                
                    #update utxsetInfo in chainstateData
                    if  len(modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk) ==5 :
                        blkhash = modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk.pop(0)
                        modules.blockchain.CBlockchain.updateUTXOset(blkhash)
                    # modules.blockchain.CBlockchain.updateUTXOset(block.selfhash)
                                
                    #update blkinfo in chainIndexdata
                    tipBlockInf= dto.tipblkInfo.CTipblkInfo(block.height,block.selfhash,storefilename)
                    modules.db.CDb.setValue("chainIndexFile","tipBlock",tipBlockInf.serializeToJsonStr())
                    modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk.append(block.selfhash)
                    modules.transaction.CTransaction.removalTxInTxsPool(block.transactions)
                    modules.transaction.CTransaction.updatetotalstransactionnumber(block.txcounts)
                    
                    if len(modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk)==5:
                        modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk.pop(0)
                    block=block.serializeToPyObject()  
                    modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk.append(block)

                    #广播区块
                    if ifBroadcast:
                        modules.net.pynet.CP2pnet.broadcast("blk",block,fromNodeAddress)
                    return 2,True
                else: 
                    return -2,False
            # else:
            #     return False

        
        #如果该block是产生分叉的区块
        elif blocktype == 3:
            # if block.valid():
                
                storefilename = dao.blockStore.CBlockStore.writeBlockToDisk(block)
                if isinstance(storefilename,str):
                    #update txinfo in chainIndexdata
                    for index, tx in enumerate(block.transactions):
                        key = tx['selfhash']
                        timestamp=tx['timestamp']
                        value = f'{storefilename},{block.selfhash},{index},{block.height},{timestamp}'
                        modules.db.CDb.setValue("chainIndexFile",key,value)
                    #广播区块
                    if ifBroadcast:
                        modules.net.pynet.CP2pnet.broadcast("blk",block.serializeToPyObject(),fromNodeAddress)

                    return 3,True
                else:
                    return -2,False
            # else:
            #     return False     
        #加入block后分支的tipblock的高度等于当前主链的高度，分支与当前主链高度并驾齐驱
        elif blocktype == 4:
            # if block.valid():
                storefilename = dao.blockStore.CBlockStore.writeBlockToDisk(block)
                if isinstance(storefilename,str):
             
                    #广播区块
                    if ifBroadcast:
                        modules.net.pynet.CP2pnet.broadcast("blk",block.serializeToPyObject(),fromNodeAddress)
                    return 4,True
                else:
                    return -2,False
            # else:
            #     return False
        
        #加入block后分叉上的区块高度赶超当前主链的高度
        elif blocktype == 5:
            # if block.valid():
                storefilename = dao.blockStore.CBlockStore.writeBlockToDisk(block)
                if isinstance(storefilename,str):
                    
                    commonblkhash,commonblkheight= self.findCommonBlockOnMainChain(block.prevBlockHash,block.height - 1)
                    if commonblkhash !="":
                        modules.transaction.CTransaction.updatetotalstransactionnumber1(modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk[-1],commonblkhash)
                        commonblk = self.getBlockFromDiskByhash(commonblkhash)
                        t = modules.blockchain.CBlockchain.getChainHeight() - commonblk.height
                            
                        if t <= 5:
                            modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk = modules.blockchain.CBlockchain.indexBlockHashfromCurrentBLock(block.prevBlockHash,block.height-1,t)
                        else:
                            t =  t-5
                            blkhash= modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk[0]
                            blkhash = modules.db.CDb.getValueByKey("chainIndexFile",blkhash).split(',')[1]
                            while t > 0:
                                originblock =self.getBlockFromDiskByhash(blkhash,3)
                                revblock = self.getBlockFromDiskByhash(blkhash,3)
                                modules.blockchain.CBlockchain.rollback(revblock,originblock)
                                blkhash = modules.db.CDb.getValueByKey("chainIndexFile",blkhash).split(',')[1]
                                t = t-1
                            
                            modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk = modules.blockchain.CBlockchain.indexBlockHashfromCurrentBLock(block.selfhash,block.height,5)

                            blockshash = []
                            #path = modules.appconfig.CAppconfig.configFromFile['chainFilePath']
                            blkhash= modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk[0]

                            blkhash = modules.db.CDb.getValueByKey("chainIndexFile",blkhash).split(',')[1]
                            while blkhash!= commonblk.selfhash:
                                blockshash.insert(0,blkhash)
                                blkhash=modules.db.CDb.getValueByKey("chainIndexFile",blkhash).split(',')[1]
                            #update utxsetInfo in chainstateData
                            while len(blockshash):
                                blkhash = blockshash.pop(0)
                                modules.blockchain.CBlockchain.updateUTXOset(blkhash)



                        modules.blockchain.CBlockchain.last_block=block.serializeToPyObject()
                        #update utxsetInfo in chainstateData
                        if  len(modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk) ==5 :
                            blkhash = modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk.pop(0)
                            modules.blockchain.CBlockchain.updateUTXOset(blkhash)
                        # modules.blockchain.CBlockchain.updateUTXOset(block.selfhash)
                                    
                        #update blkinfo in chainIndexdata
                        tipBlockInf= dto.tipblkInfo.CTipblkInfo(block.height,block.selfhash,storefilename)
                        modules.db.CDb.setValue("chainIndexFile","tipBlock",tipBlockInf.serializeToJsonStr())
                        modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk.append(block.selfhash)
                        modules.transaction.CTransaction.removalTxInTxsPool(block.transactions)

                        modules.transaction.CTransaction.updatetotalstransactionnumber(block.txcounts)
                        
                        if len(modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk)==5:
                            modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk.pop(0)
                        block=block.serializeToPyObject()  
                        modules.blockchain.CBlockchain.fiveNewBlockDequeFromTheTipBlk.append(block)

                        #广播区块
                        if ifBroadcast:
                            modules.net.pynet.CP2pnet.broadcast("blk",block,fromNodeAddress)
                        return 5,True
                    else:
                        return -2,False
                else:
                    return -2,False  
            # else:
            #     return False
        #===================================block from net  和其它节点同步时==========================================================
        #加入block的高度小于当前主链的高度
        elif blocktype == 6:
            # if block.valid():
                storefilename = dao.blockStore.CBlockStore.writeBlockToDisk(block)
                if isinstance(storefilename,str):
                
                    #广播区块
                    if ifBroadcast:
                        modules.net.pynet.CP2pnet.broadcast("blk",block.serializeToPyObject(),fromNodeAddress)
                    return 6,True
                else:
                    return -2,False
            # else:
            #      return False

    def ifBlockIsStoredLocal(self,queryBlockhash):
       
        storeinfo = modules.db.CDb.getValueByKey("chainIndexFile",queryBlockhash)
        if storeinfo!=None:
            return True,storeinfo
        else:
            return False,""


    def findCommonBlockOnMainChain(self,queryBlockhash,queryBlockHeight):
        tipHeight = modules.blockchain.CBlockchain.getChainHeight() 
        if tipHeight >= queryBlockHeight:
            blkhashOnMainChain,flag = self.getBlockHashByHeightFromTheMainChainTip(queryBlockHeight)
            if queryBlockhash == blkhashOnMainChain and flag == 1:
                return queryBlockhash,queryBlockHeight
            else:
                height = queryBlockHeight
                blkhash1 = queryBlockhash
                blkhash2 = blkhashOnMainChain
                while height >0:
                    isStoredLocal1,storeInfo1 = self.ifBlockIsStoredLocal(blkhash1)
                    if isStoredLocal1:
                        isStoredLocal2,storeInfo2= self.ifBlockIsStoredLocal(blkhash2)
                        if isStoredLocal2:
                            storeInfolist1 = storeInfo1.split(",")
                            storeInfolist2 = storeInfo2.split(",")
                            if storeInfolist1[1] == storeInfolist2[1]:
                                return storeInfolist1[1],height-1
                            else:
                                blkhash1 = storeInfolist1[1]
                                blkhash2 = storeInfolist2[1]
                                height = height -1
                    else:
                        return "",-1
                return "",-1
        else:
             return "",-2

    def getBlockHashByHeightFromTheMainChainTip(self,queryheight):
        height = modules.blockchain.CBlockchain.getChainHeight()
        if queryheight <= height:
            t = height - queryheight
            t1 = len(modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk)  
            if t < t1:
                return modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk[-1-t],1
            else:
                height = modules.blockchain.CBlockchain.getChainHeight() - len(modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk) +1
                lastblkhash = modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk[0]
                blockhash=""
                while height > queryheight  and height>1:
                    
                    blkStoreInfo = modules.db.CDb.getValueByKey("chainIndexFile",lastblkhash)
                    if blkStoreInfo !=None:
                        blkStoreInfo = blkStoreInfo.split(",")
                        blockhash = blkStoreInfo[1]
                        lastblkhash = blkStoreInfo[1]
                        height = int(blkStoreInfo[0])-1
                    else:
                        return "",-2

                return blockhash,1
                            
        else:
            return "",-1
    def ifIsInMainChain(self,queryBlockhash,queryBlockHeight):
        isStoredLocal,storeInfo = self.ifBlockIsStoredLocal(queryBlockhash)
        if isStoredLocal:
            if queryBlockhash in modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk:
                return True,1
            else:
                height = modules.blockchain.CBlockchain.getChainHeight() - len(modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk) +1
                lastblkhash = modules.blockchain.CBlockchain.fivePreBlockhashDequeFromTheTipBlk[0]

                while height > queryBlockHeight:
                    
                    blkStoreInfo = modules.db.CDb.getValueByKey("chainIndexFile",lastblkhash)
                    if blkStoreInfo !=None:
                        blkStoreInfo = blkStoreInfo.split(",")
                        if queryBlockhash == blkStoreInfo[1]:
                            return True,2
                        else:
                            height = int(blkStoreInfo[0]) -1
                            lastblkhash = blkStoreInfo[1]
                    else:
                        return False,-1
                            
                return False,-2
        else:
            return False,-3

    def getBlockFromDiskByhash(self,blkhash,blktype=2):
        """
        blktype =2   普通block
        blktype =3    revblock
        """
        if blktype==3:
            storefilename=f'{blkhash[0:2]}/{blkhash}-rev.dat'
        else:
            storefilename=f'{blkhash[0:2]}/{blkhash}.dat'

        
        # storefilename = modules.db.CDb.getValueByKey("chainIndexFile",blkhash).split(',')[blktype]
        block = self.getBlockFromDiskByFileName(storefilename)
        return block
    
    def getBlockFromDiskByFileName(self,storefilename):
         path = modules.appconfig.CAppconfig.configFromFile['chainFilePath']
         filename = f'{path}{storefilename}'
         block = dto.block.CBlock.deserializeFromJsonfile(filename)
         return block
    def validBlock(self,block):
        """
        @block 为python object (dict)
        """
        block=copy.deepcopy(block)
        
        oldhash = block['selfhash']
        # block['selfhash'] =""
        block = dto.block.CBlock.deserializeFromPyObject(block)
        hash1 = util.chash.CHash.getBlockHash(block)
        # block['selfhash'] = oldhash
        if hash1 == oldhash:
            for tx in block.transactions:
                flag= modules.transaction.CTransaction.validTranscation(tx)
                return flag
            return True
        else:
            return False